下面使用windbg是新版的windbg,叫windbg preview,需要在微软商店(Microsoft Store)中下载安装
新版的windbg具有更现代的视觉效果,速度更快,更完善的脚本编写体验以及Time Travel Debugging 特性
需要注意的是,Time Travel Debugging特性的开启需要以管理员权限运行,之后使用advanced打开并勾选对应的选项即可,最后配置一下记录的保存路径即可
当然你也可以attach到正在运行的程序,或者打开别人的trace file
有了Time Travel Debugging,你几乎可以像本地看电影一样,不断来回脱动进度条,而且这个还是本地化,免去了查找漏洞软件,搭建环境,验证漏洞的烦恼,只需要拿到别人的TTD的记录文件即可,而且调试过程地址还不会变,爽不爽。
Time Travel Debugging 初探
我们随便找一个windows程序,看看这个新特性
一开始载入没什么反应,以为出什么事了,原来是这个特性载入程序,速度太慢了
对于我们常用的p,t,g,假如想反向执行,只需在指令后面加个”-“号
1 | 0:000> p |
可以看到,假如我们一步小心,执行过了,还可以回退,可以回到当初的状态看看寄存器,内存,这是相当好的东西,有后悔药吃了。
有时候我们执行过了头,想要跟进去那个函数,也是可以回头进去的
1 | eax=12fd94d0 ebx=02bfe000 ecx=009913a0 edx=00000000 esi=0098f9c0 edi=0098f9bc |
我们看看记录的目录,共有三个文件,.run是trace文件,idx是索引文件,索引文件可以让我们更快地访问跟踪信息。假如我们只有.run文件,也是可以的,当WinDbg Preview打开跟踪文件(.run)时,索引文件也会自动创建。
利用别人的跟踪文件调试
这是参考文章的作者给的文件,一个.run文件就592M了,但是可以免除搭建环境,1个G甚至8个G我都能接受
这个漏洞是Adobe Acrobat Reader DC for Windows - Double Free due to Malformed JP2 Stream(CVE-2019-8044),POC可以在下面下载
https://www.exploit-db.com/exploits/47279
我就不下载POC复现了,直接用作者的跟踪文件分析,载入run文件后,直接g,触发崩溃
1 | Time Travel Position: 224FB2:1 |
可以看到这里面看不到什么有用的东西,都是windows的异常处理函数相关的,而且我们通过信息已经知道这个漏洞是double free了,可能是free的时候崩溃的,我们需要向上回溯
通过不断点击step out back,或者命令g-u
去不断返回,但是我们到ntdll!KiUserExceptionDispatcher+0xf
的时候再返回,就变成载入时的状态,所以到这一步我们应该点击step over back,或者命令p-
,多step over back几次即可
最终可以执行下面命令回到真正触发崩溃的地方
1 | g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;g-u;p-;p-;p-;p-;p-;p-; |
再看下此时的栈,是MSVCR120!free之后调用ntdll!RtlFreeHeap失败,最终进入ntdll!RtlpLogHeapFailure处理
1 | 0:001> kb |
我们看看什么导致checkfail了
1 | Time Travel Position: 222B02:6F |
原来是这个77097851 f646073f test byte ptr [esi+7],3Fh ds:002b:1fb1c84f=80
这个地址肯定被改写了才导致检查是吧,所以我们可以对这个地址下一个写入断点,之后往回走,g-就行,实在太方便了
1 | ba w1 1fb1c84f |
由于这个断下来会停在写入的下一个指令,所以还要p-一下
1 | 0:001> p- |
这时再看一下栈,可以看到MSVCR120!free(void * pBlock = 0x1fb1c850)+0x1a
,是free(0x1fb1c850)
后导致的写入,而且这个栈跟上面的有所区别,所以这应该是第一次free,写入了0x80后,导致第二次free失败了。
1 | 0:001> kp |
参考文章的作者还给出了TTD的一个牛逼的命令,可以看到MSVCR120!malloc什么时候返回0x1fb1c850
注:下面中括号的四个可能会变
1 | 0:001> dx -r1 @$cursession.TTD.Calls("MSVCR120!malloc").Where(c => c.ReturnValue == (void *)0x1fb1c850) |
之后可以在后面加个中括号去访问对应的那个状态,可以看到他的时间点去判断是在free之前malloc的还是之后(下面的TimeStart)
1 | 0:001> dx -r1 @$cursession.TTD.Calls("MSVCR120!malloc").Where(c => c.ReturnValue == (void *)0x1fb1c850)[0x231fe] |
或者直接点击那是个中括号的值(点击的话中括号的会变成十进制的)
1 | 0:001> dx -r1 @$cursession.TTD.Calls("MSVCR120!malloc").Where(c => c.ReturnValue == (void *)0x1fb1c850)[143870] |
写入0x80的时间是Time Travel Position: 222B02:B
(第一次free),所以malloc应该在他前面且最近的一次
上面四个分别是下面的,所以最近的应该是最后一个
1 | TimeStart : 20FB5D:162 [Translate Position] |
跟参考文章作者得出的一样,只不过那个中括号的排序可能有所差异
感觉这样还没有条件记录断点方便,但是还是挺清晰的
跟踪free
到上面写入0x80这,我们已经在第一次free的里面了
向上回溯,到第一次调用free的地方(AcroRd32!AcroWinBrowserMain+0x2593里面会调用MSVCR120!free)
1 | 0:001> p- |
而我们看看现在代码上下,发现两次调用了AcroRd32!AcroWinBrowserMain+0x2593
1 | 6a18b286 8b8568ffffff mov eax, dword ptr [ebp-98h] |
我们向下执行(记得将写入断点禁用),看看是不是
1 | 6a18b286 8b8568ffffff mov eax, dword ptr [ebp-98h] |
可以看到[ebp-90h]里面存的还是1fb1c850,那就明显free了两次了
看下内存更清晰
1 | 0:001> dd ebp-98 l4 |
最终在第二个 call AcroRd32!AcroWinBrowserMain+0x2593 (69bfa0b7)
里面崩溃,确实free两次了
1 | 0:001> p |
那么这个[ebp-98h]和[ebp-90h]从哪里来的呢,由于这个是4字节的,我下一个4字节的写入断点
1 | 0:001> ba w4 010cda74 |
这两个代码很接近,我们看看上下文
1 | 6a18ac3f 56 push esi |
这个函数其实是GetMemoryBlock函数
1 | undefined4 |
我们看看两次调用的6个参数
1 | eax=010cdab4 ebx=00000000 ecx=1fb1c700 edx=00000003 esi=1fb1c700 edi=00000000 |
可以看到两次调用,只是第一个参数不一样,第一次是0x01000000,第二次是0x00010000,但是返回结果是一样的,而函数GetMemoryBlock里面是将memory_block_type强制转为char进行处理的,所以他们都是0x00,导致返回结果一样。
执行的是else部分代码
1 | else { |
最终offset = offsetVal1 = 0x0000000e
而retVal = *(memory_block_base + 0x20 + (uint)offset * 4 = 0x016afde0 + 0x20 + 0x0000000e * 4) = *(0x16afe38)
我们看看这个地址的值,的确是0x1fb1c850
1 | 0:001> dd 0x16afe38 l1 |
跟踪0x00010000来源
1 | eax=010cdab4 ebx=00000000 ecx=016afde0 edx=00000003 esi=1fb1c700 edi=00000000 |
对 [ebp-1Bh]下吸入断点
1 | 0:001> ba w1 010cdaf1 |
这里是010cdb54指向的值吸入010cdaf4,那么010cdaf1就来源于010cdb51,看了下确实是0x00010000
1 | 0:001> dd 010cdb51 l1 |
继续不断下硬件写入断点,跟踪010cdb51,来源于0x010cdbd1,
1 | 0:001> ba w1 010cdb51 |
这个往上看看,写入的00来源于0x1fbad895
1 | 6a17b85f 8a437d mov al, byte ptr [ebx+7Dh] ds:002b:1fbad895=00 |
后面有两个字节来源于别处,而且都是0
1 | a17b86b 8885fefeffff mov byte ptr [ebp-102h], al |
只有ebp-101没被修改,而这里本来就是01了
1 | 0:001> db ebp-101 l1 |
而我们关注的是最低字节,继续看看1fbad895的写入
1 | 0:001> ba w1 1fbad895 |
向上看
1 | 6a17be18 e809d5ffff call AcroRd32!CTJPEGTiledContentWriter::operator=+0xd02 (6a179326) |
上面说明分析看出来源于call AcroRd32!CTJPEGTiledContentWriter::operator=+0xd02 (6a179326)
的返回值,跟进去看看,分析可以看出实际来源于0x0763beac
1 | 6a179358 0fb608 movzx ecx, byte ptr [eax] ds:002b:0763beac=00 |
看看里面
1 | 0:001> db 0763beac |
通过ba w1 0763beac,发现0x0763beac来源于0x0746ba24,之后就不知道0x0746ba24来源于什么的
而0763beac我们也在poc中找到了这段内容,那这就是从文件读取进来的了
最终再看这张图就很清晰了
总结
漏洞就是经过读取文件,计算,调用GetMemoryBlock,由于最低位都是00,导致获取的指针都指向同一个内存(文中的0x1fb1c850),而free的时候就free两次了。
TTD虽然优点很明显
1、可以无poc,无环境调试漏洞,可以团队协作共享
2、地址都是不变的
3、能够”时间穿梭”
缺点
1、假如程序过大,跟踪文件可能很大,而且调试也是比较占用内存的
2、原始调试者的本地目录可能随着分享的跟踪文件而泄露
参考
https://darungrim.com/research/2019-10-10-vulnerability-root-cause-analysis-with-time-travel-debugging.html
https://www.exploit-db.com/exploits/47279
https://github.com/offensive-security/exploitdb-bin-sploits/raw/master/bin-sploits/47279.zip